/*
 * Copyright 2004-2014 SmartBear Software
 *
 * Licensed under the EUPL, Version 1.1 or - as soon as they will be approved by the European Commission - subsequent
 * versions of the EUPL (the "Licence");
 * You may not use this work except in compliance with the Licence.
 * You may obtain a copy of the Licence at:
 *
 * http://ec.europa.eu/idabc/eupl
 *
 * Unless required by applicable law or agreed to in writing, software distributed under the Licence is
 * distributed on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the Licence for the specific language governing permissions and limitations
 * under the Licence.
*/

package org.syntax.jedit;

import java.awt.AWTEvent;
import java.awt.BorderLayout;
import java.awt.Component;
import java.awt.Container;
import java.awt.Dimension;
import java.awt.Font;
import java.awt.FontMetrics;
import java.awt.Insets;
import java.awt.LayoutManager;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.Toolkit;
import java.awt.datatransfer.Clipboard;
import java.awt.datatransfer.DataFlavor;
import java.awt.datatransfer.StringSelection;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.AdjustmentEvent;
import java.awt.event.AdjustmentListener;
import java.awt.event.ComponentAdapter;
import java.awt.event.ComponentEvent;
import java.awt.event.FocusEvent;
import java.awt.event.FocusListener;
import java.awt.event.InputEvent;
import java.awt.event.KeyEvent;
import java.awt.event.KeyListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.awt.event.MouseMotionListener;
import java.awt.event.MouseWheelEvent;
import java.awt.event.MouseWheelListener;
import java.lang.ref.WeakReference;

import javax.swing.JComponent;
import javax.swing.JPopupMenu;
import javax.swing.JViewport;
import javax.swing.Scrollable;
import javax.swing.SwingUtilities;
import javax.swing.Timer;
import javax.swing.event.CaretEvent;
import javax.swing.event.CaretListener;
import javax.swing.event.DocumentEvent;
import javax.swing.event.DocumentListener;
import javax.swing.event.EventListenerList;
import javax.swing.text.BadLocationException;
import javax.swing.text.Element;
import javax.swing.text.Segment;
import javax.swing.text.Utilities;
import javax.swing.undo.AbstractUndoableEdit;
import javax.swing.undo.CannotRedoException;
import javax.swing.undo.CannotUndoException;
import javax.swing.undo.UndoableEdit;

import org.syntax.jedit.tokenmarker.Token;
import org.syntax.jedit.tokenmarker.TokenMarker;

import com.eviware.soapui.SoapUI;

/**
 * jEdit's text area component. It is more suited for editing program source
 * code than JEditorPane, because it drops the unnecessary features (images,
 * variable-width lines, and so on) and adds a whole bunch of useful goodies
 * such as:
 * <ul>
 * <li>More flexible key binding scheme
 * <li>Supports macro recorders
 * <li>Rectangular selection
 * <li>Bracket highlighting
 * <li>Syntax highlighting
 * <li>Command repetition
 * <li>Block caret can be enabled
 * </ul>
 * It is also faster and doesn't have as many problems. It can be used in other
 * applications; the only other part of jEdit it depends on is the syntax
 * package.
 * <p/>
 * <p/>
 * To use it in your app, treat it like any other component, for example:
 * <p/>
 * <pre>
 * JEditTextArea ta = new JEditTextArea();
 * ta.setTokenMarker( new JavaTokenMarker() );
 * ta.setText( &quot;public class Test {\n&quot; + &quot;    public static void main(String[] args) {\n&quot;
 * 		+ &quot;        System.out.println(\&quot;Hello World\&quot;);\n&quot; + &quot;    }\n&quot; + &quot;}&quot; );
 * </pre>
 *
 * @author Slava Pestov
 * @version $Id$
 */
public class JEditTextArea extends JComponent implements Scrollable {
    /**
     * Adding components with this name to the text area will place them left of
     * the horizontal scroll bar. In jEdit, the status bar is added this way.
     */
    public final static String LEFT_OF_SCROLLBAR = "los";

    /**
     * Creates a new JEditTextArea with the default settings.
     */
    public JEditTextArea() {
        this(TextAreaDefaults.getDefaults());
    }

    /**
     * Creates a new JEditTextArea with the specified settings.
     *
     * @param defaults The default settings
     */
    public JEditTextArea(TextAreaDefaults defaults) {
        // Enable the necessary events
        enableEvents(AWTEvent.KEY_EVENT_MASK);

        // Initialize some misc. stuff
        painter = createPainter(defaults);
        documentHandler = new DocumentHandler();
        listenerList = new EventListenerList();
        caretEvent = new MutableCaretEvent();
        lineSegment = new Segment();
        bracketLine = bracketPosition = -1;
        blink = true;

        setAutoscrolls(true);

        // Initialize the GUI

        // setLayout(new ScrollLayout());
        // add(CENTER,painter);
        setLayout(new BorderLayout());
        add(painter, BorderLayout.CENTER);
        // setBackground( Color.WHITE );
        // setBorder( null );
        // add(RIGHT,vertical = new JScrollBar(JScrollBar.VERTICAL));
        // add(BOTTOM,horizontal = new JScrollBar(JScrollBar.HORIZONTAL));

        // Add some event listeners
        // vertical.addAdjustmentListener(new AdjustHandler());
        // horizontal.addAdjustmentListener(new AdjustHandler());
        painter.addComponentListener(new ComponentHandler());
        painter.addMouseListener(new MouseHandler());
        painter.addMouseMotionListener(new DragHandler());
        addFocusListener(new FocusHandler());

        // Load the defaults
        setInputHandler(defaults.inputHandler);
        setDocument(defaults.document);
        editable = defaults.editable;
        caretVisible = defaults.caretVisible;
        caretBlinks = defaults.caretBlinks;
        // electricScroll = defaults.electricScroll;

        popup = defaults.popup;

        // We don't seem to get the initial focus event?
        focusedComponentRef = new WeakReference<JEditTextArea>(this);

        addMouseWheelListener(new MouseWheelListener() {

            public void mouseWheelMoved(MouseWheelEvent e) {
                if ((e.getModifiers() & Toolkit.getDefaultToolkit().getMenuShortcutKeyMask()) == Toolkit
                        .getDefaultToolkit().getMenuShortcutKeyMask()) {
                    int caretLine = getCaretLine();
                    // int caretPosition = getCaretPosition();

                    int newLine = caretLine + e.getWheelRotation();
                    if (newLine < 0) {
                        newLine = 0;
                    } else if (newLine > getLineCount() - 1) {
                        newLine = getLineCount() - 1;
                    }
                    int newPos = getLineStartOffset(newLine);

                    setCaretPosition(newPos);
                } else {
                    Rectangle rect = getVisibleRect();
                    rect.setLocation((int) rect.getX(),
                            (int) rect.getY() + painter.getFontMetrics().getHeight() * 3 * e.getWheelRotation());
                    scrollRectToVisible(rect);
                }
            }
        });
    }

    /**
     * Allow subclasses to override.
     *
     * @param defaults
     * @return
     * @author lars
     */
    protected TextAreaPainter createPainter(TextAreaDefaults defaults) {
        return new TextAreaPainter(this, defaults);
    }

    /**
     * Returns if this component can be traversed by pressing the Tab key. This
     * returns false.
     */
    public final boolean isManagingFocus() {
        return true;
    }

    /**
     * Returns the object responsible for painting this text area.
     */
    public final TextAreaPainter getPainter() {
        return painter;
    }

    /**
     * Returns the input handler.
     */
    public final InputHandler getInputHandler() {
        return inputHandler;
    }

    /**
     * Sets the input handler.
     *
     * @param inputHandler The new input handler
     */
    public void setInputHandler(InputHandler inputHandler) {
        this.inputHandler = inputHandler;
    }

    /**
     * Returns true if the caret is blinking, false otherwise.
     */
    public final boolean isCaretBlinkEnabled() {
        return caretBlinks;
    }

    /**
     * Toggles caret blinking.
     *
     * @param caretBlinks True if the caret should blink, false otherwise
     */
    public void setCaretBlinkEnabled(boolean caretBlinks) {
        this.caretBlinks = caretBlinks;
        if (!caretBlinks) {
            blink = false;
        }

        painter.invalidateSelectedLines();
    }

    /**
     * Returns true if the caret is visible, false otherwise.
     */
    public final boolean isCaretVisible() {
        return (!caretBlinks || blink) && caretVisible;
    }

    /**
     * Sets if the caret should be visible.
     *
     * @param caretVisible True if the caret should be visible, false otherwise
     */
    public void setCaretVisible(boolean caretVisible) {
        this.caretVisible = caretVisible;
        blink = true;

        painter.invalidateSelectedLines();
    }

    /**
     * Blinks the caret.
     */
    public final void blinkCaret() {
        if (caretBlinks && caretVisible) {
            blink = !blink;
            painter.invalidateSelectedLines();
        } else {
            blink = true;
        }
    }

    /**
     * Returns the number of lines from the top and button of the text area that
     * are always visible.
     */
    /*
	 * public final int getElectricScroll() { return electricScroll; }
	 */

    /**
     * Sets the number of lines from the top and bottom of the text area that are
     * always visible
     *
     * @param electricScroll
     *           The number of lines always visible from the top or bottom
     */
	/*
	 * public final void setElectricScroll(int electricScroll) {
	 * this.electricScroll = electricScroll; }
	 */

    /**
     * Updates the state of the scroll bars. This should be called if the number
     * of lines in the document changes, or when the size of the text area
     * changes.
     */
    public void updateScrollBars() {
        revalidate();
    }

    /**
     * Returns the line displayed at the text area's origin.
     */
    public final int getFirstLine() {
        return firstLine;
    }

    /**
     * Sets the line displayed at the text area's origin without updating the
     * scroll bars.
     */
    public void setFirstLine(int firstLine) {
        if (firstLine == this.firstLine) {
            return;
        }
        // int oldFirstLine = this.firstLine;
        this.firstLine = firstLine;
        // if(firstLine != vertical.getValue())
        updateScrollBars();
        painter.repaint();
    }

    /**
     * Returns the number of lines visible in this text area.
     */
    public final int getVisibleLines() {
        return visibleLines;
    }

    /**
     * Recalculates the number of visible lines. This should not be called
     * directly.
     */
    public final void recalculateVisibleLines() {
        if (painter == null) {
            return;
        }
        int height = painter.getHeight();
        int lineHeight = painter.getFontMetrics().getHeight();
        visibleLines = height / lineHeight;
        updateScrollBars();
    }

    /**
     * Returns the horizontal offset of drawn lines.
     */
	/*
	 * public final int getHorizontalOffset() { return horizontalOffset; }
	 */

    /**
     * Sets the horizontal offset of drawn lines. This can be used to implement
     * horizontal scrolling.
     *
     * @param horizontalOffset
     *           offset The new horizontal offset
     */
	/*
	 * public void setHorizontalOffset(int horizontalOffset) {
	 * if(horizontalOffset == this.horizontalOffset) return;
	 * this.horizontalOffset = horizontalOffset; // if(horizontalOffset !=
	 * horizontal.getValue()) updateScrollBars(); painter.repaint(); }
	 */

    /**
     * A fast way of changing both the first line and horizontal offset.
     *
     * @param firstLine
     *           The new first line
     * @param horizontalOffset
     *           The new horizontal offset
     * @return True if any of the values were changed, false otherwise
     */
	/*
	 * public void setOrigin(int firstLine, int horizontalOffset) { boolean
	 * changed = false; int oldFirstLine = this.firstLine;
	 * 
	 * if(horizontalOffset != this.horizontalOffset) { this.horizontalOffset =
	 * horizontalOffset; changed = true; }
	 * 
	 * if(firstLine != this.firstLine) { this.firstLine = firstLine; changed =
	 * true; }
	 * 
	 * if(changed) { scrollRectToVisible( new Rectangle( horizontalOffset,
	 * firstLine*painter.getFontMetrics().getHeight(), 1, 1));
	 * 
	 * updateScrollBars(); painter.repaint(); //}
	 * 
	 * // return changed; }
	 */

    /**
     * Ensures that the caret is visible by scrolling the text area if necessary.
     *
     * @return True if scrolling was actually performed, false if the caret was
     *         already visible
     */
    public void scrollToCaret() {
        int line = getCaretLine();
        int lineStart = getLineStartOffset(line);
        int offset = Math.max(0, Math.min(getTabExpandedLineLength(line) - 1, getCaretPosition() - lineStart));

        scrollTo(line, offset);
    }

    /**
     * Ensures that the specified line and offset is visible by scrolling the
     * text area if necessary.
     *
     * @param line   The line to scroll to
     * @param offset The offset in the line to scroll to
     * @return True if scrolling was actually performed, false if the line and
     *         offset was already visible
     */
    public void scrollTo(int line, int offset) {
        // visibleLines == 0 before the component is realized
        // we can't do any proper scrolling then, so we have
        // this hack...
		/*
		 * if(visibleLines == 0) { setFirstLine(Math.max(0,line -
		 * electricScroll)); return true; }
		 * 
		 * int newFirstLine = firstLine; int newHorizontalOffset =
		 * horizontalOffset;
		 * 
		 * if(line < firstLine + electricScroll) { newFirstLine = Math.max(0,line
		 * - electricScroll); } else if(line + electricScroll >= firstLine +
		 * visibleLines) { newFirstLine = (line - visibleLines) + electricScroll +
		 * 1; if(newFirstLine + visibleLines >= getLineCount()) newFirstLine =
		 * getLineCount() - visibleLines; if(newFirstLine < 0) newFirstLine = 0; }
		 */

        int x = _offsetToX(line, offset);
        int width = painter.getFontMetrics().charWidth('w');
		/*
		 * if(x < 0) { newHorizontalOffset = Math.min(0,horizontalOffset - x +
		 * width + 5); } else if(x + width >= getVisibleRect().getWidth() ) {
		 * newHorizontalOffset = horizontalOffset +
		 * (x-(int)getVisibleRect().getWidth()) + width + 5; }
		 */
        if (offset > 0) {
            x += (width + 5);
        }

        int y = lineToY(line);
        if (line > 0) {
            y += 5;
        }

        if (line > 0) {
            line++;
        }

        scrollRectToVisible(new Rectangle(x, y, 1, painter.getFontMetrics().getHeight()));

        updateScrollBars();
        painter.repaint();

        // setOrigin(line, x);
    }

    /**
     * Converts a line index to a y co-ordinate.
     *
     * @param line The line
     */
    public int lineToY(int line) {
        FontMetrics fm = painter.getFontMetrics();
        return (line - firstLine) * fm.getHeight() - (fm.getLeading() + fm.getMaxDescent());
    }

    public int getLineHeight() {
        FontMetrics fm = painter.getFontMetrics();
        return fm.getHeight();
    }

    /**
     * Converts a y co-ordinate to a line index.
     *
     * @param y The y co-ordinate
     */
    public int yToLine(int y) {
        FontMetrics fm = painter.getFontMetrics();
        int height = fm.getHeight();
        return Math.max(0, Math.min(getLineCount() - 1, y / height + firstLine));
    }

    /**
     * Converts an offset in a line into an x co-ordinate. This is a slow version
     * that can be used any time.
     *
     * @param line   The line
     * @param offset The offset, from the start of the line
     */
    public final int offsetToX(int line, int offset) {
        // don't use cached tokens
        painter.currentLineTokens = null;
        return _offsetToX(line, offset);
    }

    /**
     * Converts an offset in a line into an x co-ordinate. This is a fast version
     * that should only be used if no changes were made to the text since the
     * last repaint.
     *
     * @param line   The line
     * @param offset The offset, from the start of the line
     */
    public int _offsetToX(int line, int offset) {
        TokenMarker tokenMarker = getTokenMarker();

		/* Use painter's cached info for speed */
        FontMetrics fm = painter.getFontMetrics();

        getLineText(line, lineSegment);

        int segmentOffset = lineSegment.offset;
        int x = 0; // -horizontalOffset;

		/* If syntax coloring is disabled, do simple translation */
        if (tokenMarker == null) {
            lineSegment.count = offset;
            return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
        }
		/*
		 * If syntax coloring is enabled, we have to do this because tokens can
		 * vary in width
		 */
        else {
            Token tokens;
            if (painter.currentLineIndex == line && painter.currentLineTokens != null) {
                tokens = painter.currentLineTokens;
            } else {
                painter.currentLineIndex = line;
                tokens = painter.currentLineTokens = tokenMarker.markTokens(lineSegment, line);
            }

            // Toolkit toolkit = painter.getToolkit();
            Font defaultFont = painter.getFont();
            SyntaxStyle[] styles = painter.getStyles();

            for (; ; ) {
                byte id = tokens.id;
                if (id == Token.END) {
                    return x;
                }

                if (id == Token.NULL) {
                    fm = painter.getFontMetrics();
                } else {
                    fm = styles[id].getFontMetrics(defaultFont);
                }

                int length = tokens.length;

                if (offset + segmentOffset < lineSegment.offset + length) {
                    lineSegment.count = offset - (lineSegment.offset - segmentOffset);
                    return x + Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
                } else {
                    lineSegment.count = length;
                    x += Utilities.getTabbedTextWidth(lineSegment, fm, x, painter, 0);
                    lineSegment.offset += length;
                }
                tokens = tokens.next;
            }
        }
    }

    /**
     * Converts an x co-ordinate to an offset within a line.
     *
     * @param line The line
     * @param x    The x co-ordinate
     */
    public int xToOffset(int line, int x) {
        TokenMarker tokenMarker = getTokenMarker();

		/* Use painter's cached info for speed */
        FontMetrics fm = painter.getFontMetrics();

        getLineText(line, lineSegment);

        char[] segmentArray = lineSegment.array;
        int segmentOffset = lineSegment.offset;
        int segmentCount = lineSegment.count;

        int width = 0; // -horizontalOffset;

        if (tokenMarker == null) {
            for (int i = 0; i < segmentCount; i++) {
                char c = segmentArray[i + segmentOffset];
                int charWidth;
                if (c == '\t') {
                    charWidth = (int) painter.nextTabStop(width, i) - width;
                } else {
                    charWidth = fm.charWidth(c);
                }

                if (painter.isBlockCaretEnabled()) {
                    if (x - charWidth <= width) {
                        return i;
                    }
                } else {
                    if (x - charWidth / 2 <= width) {
                        return i;
                    }
                }

                width += charWidth;
            }

            return segmentCount;
        } else {
            Token tokens;
            if (painter.currentLineIndex == line && painter.currentLineTokens != null) {
                tokens = painter.currentLineTokens;
            } else {
                painter.currentLineIndex = line;
                tokens = painter.currentLineTokens = tokenMarker.markTokens(lineSegment, line);
            }

            int offset = 0;
            // Toolkit toolkit = painter.getToolkit();
            Font defaultFont = painter.getFont();
            SyntaxStyle[] styles = painter.getStyles();

            for (; ; ) {
                byte id = tokens.id;
                if (id == Token.END) {
                    return offset;
                }

                if (id == Token.NULL) {
                    fm = painter.getFontMetrics();
                } else {
                    fm = styles[id].getFontMetrics(defaultFont);
                }

                int length = tokens.length;

                for (int i = 0; i < length; i++) {
                    char c = segmentArray[segmentOffset + offset + i];
                    int charWidth;
                    if (c == '\t') {
                        charWidth = (int) painter.nextTabStop(width, offset + i) - width;
                    } else {
                        charWidth = fm.charWidth(c);
                    }

                    if (painter.isBlockCaretEnabled()) {
                        if (x - charWidth <= width) {
                            return offset + i;
                        }
                    } else {
                        if (x - charWidth / 2 <= width) {
                            return offset + i;
                        }
                    }

                    width += charWidth;
                }

                offset += length;
                tokens = tokens.next;
            }
        }
    }

    /**
     * Converts a point to an offset, from the start of the text.
     *
     * @param x The x co-ordinate of the point
     * @param y The y co-ordinate of the point
     */
    public int xyToOffset(int x, int y) {
        int line = yToLine(y);
        int start = getLineStartOffset(line);
        return start + xToOffset(line, x);
    }

    public int pointToOffset(Point pt) {
        return xyToOffset((int) pt.getX(), (int) pt.getY());
    }

    /**
     * Returns the document this text area is editing.
     */
    public final SyntaxDocument getDocument() {
        return document;
    }

    /**
     * Sets the document this text area is editing.
     *
     * @param document The document
     */
    public void setDocument(SyntaxDocument document) {
        if (this.document == document) {
            return;
        }
        if (this.document != null) {
            this.document.removeDocumentListener(documentHandler);
        }
        this.document = document;

        if (getParent() != null) {
            document.addDocumentListener(documentHandler);
        }

        select(0, 0);
        updateScrollBars();
        painter.repaint();
    }

    /**
     * Returns the document's token marker. Equivalent to calling
     * <code>getDocument().getTokenMarker()</code>.
     */
    public final TokenMarker getTokenMarker() {
        return document.getTokenMarker();
    }

    /**
     * Sets the document's token marker. Equivalent to caling
     * <code>getDocument().setTokenMarker()</code>.
     *
     * @param tokenMarker The token marker
     */
    public final void setTokenMarker(TokenMarker tokenMarker) {
        document.setTokenMarker(tokenMarker);
    }

    /**
     * Returns the length of the document. Equivalent to calling
     * <code>getDocument().getLength()</code>.
     */
    public final int getDocumentLength() {
        return document.getLength();
    }

    /**
     * Returns the number of lines in the document.
     */
    public final int getLineCount() {
        return document.getDefaultRootElement().getElementCount();
    }

    /**
     * Returns the line containing the specified offset.
     *
     * @param offset The offset
     */
    public final int getLineOfOffset(int offset) {
        return document.getDefaultRootElement().getElementIndex(offset);
    }

    /**
     * Returns the start offset of the specified line.
     *
     * @param line The line
     * @return The start offset of the specified line, or -1 if the line is
     *         invalid
     */
    public int getLineStartOffset(int line) {
        Element lineElement = document.getDefaultRootElement().getElement(line);
        if (lineElement == null) {
            return -1;
        } else {
            return lineElement.getStartOffset();
        }
    }

    /**
     * Returns the end offset of the specified line.
     *
     * @param line The line
     * @return The end offset of the specified line, or -1 if the line is
     *         invalid.
     */
    public int getLineEndOffset(int line) {
        Element lineElement = document.getDefaultRootElement().getElement(line);
        if (lineElement == null) {
            return -1;
        } else {
            return lineElement.getEndOffset();
        }
    }

    /**
     * Returns the length of the specified line.
     *
     * @param line The line
     */
    public int getTabExpandedLineLength(int line) {
        Element lineElement = document.getDefaultRootElement().getElement(line);
        if (lineElement == null) {
            return -1;
        }

        int length = lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
        try {
            String txt = document.getText(lineElement.getStartOffset(), length);

            for (int c = 0; c < txt.length(); c++) {
                if (txt.charAt(c) == '\t') {
                    length += 7;
                }
            }

            return length;
        } catch (BadLocationException e) {
            e.printStackTrace();
            return length;
        }
    }

    /**
     * Returns the length of the specified line.
     *
     * @param line The line
     */
    public int getLineLength(int line) {
        Element lineElement = document.getDefaultRootElement().getElement(line);
        if (lineElement == null) {
            return -1;
        }

        return lineElement.getEndOffset() - lineElement.getStartOffset() - 1;
    }

    /**
     * Returns the entire text of this text area.
     */
    public String getText() {
        try {
            return document.getText(0, document.getLength());
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
            return null;
        }
    }

    /**
     * Sets the entire text of this text area.
     */
    public synchronized void setText(String text) {
        try {
            document.beginCompoundEdit();
            document.remove(0, document.getLength());
            document.insertString(0, text, null);

            revalidate();
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
        } finally {
            document.endCompoundEdit();
        }
    }

    /**
     * Returns the specified substring of the document.
     *
     * @param start The start offset
     * @param len   The length of the substring
     * @return The substring, or null if the offsets are invalid
     */
    public final String getText(int start, int len) {
        try {
            return document.getText(start, len);
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
            return null;
        }
    }

    /**
     * Copies the specified substring of the document into a segment. If the
     * offsets are invalid, the segment will contain a null string.
     *
     * @param start   The start offset
     * @param len     The length of the substring
     * @param segment The segment
     */
    public final void getText(int start, int len, Segment segment) {
        try {
            document.getText(start, len, segment);
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
            segment.offset = segment.count = 0;
        }
    }

    /**
     * Returns the text on the specified line.
     *
     * @param lineIndex The line
     * @return The text, or null if the line is invalid
     */
    public final String getLineText(int lineIndex) {
        int start = getLineStartOffset(lineIndex);
        return getText(start, getLineEndOffset(lineIndex) - start - 1);
    }

    /**
     * Copies the text on the specified line into a segment. If the line is
     * invalid, the segment will contain a null string.
     *
     * @param lineIndex The line
     */
    public final void getLineText(int lineIndex, Segment segment) {
        int start = getLineStartOffset(lineIndex);
        getText(start, getLineEndOffset(lineIndex) - start - 1, segment);
    }

    /**
     * Returns the selection start offset.
     */
    public final int getSelectionStart() {
        return selectionStart;
    }

    /**
     * Returns the offset where the selection starts on the specified line.
     */
    public int getSelectionStart(int line) {
        if (line == selectionStartLine) {
            return selectionStart;
        } else if (rectSelect) {
            Element map = document.getDefaultRootElement();
            int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();

            Element lineElement = map.getElement(line);
            int lineStart = lineElement.getStartOffset();
            int lineEnd = lineElement.getEndOffset() - 1;
            return Math.min(lineEnd, lineStart + start);
        } else {
            return getLineStartOffset(line);
        }
    }

    /**
     * Returns the selection start line.
     */
    public final int getSelectionStartLine() {
        return selectionStartLine;
    }

    /**
     * Sets the selection start. The new selection will be the new selection
     * start and the old selection end.
     *
     * @param selectionStart The selection start
     * @see #select(int, int)
     */
    public final void setSelectionStart(int selectionStart) {
        select(selectionStart, selectionEnd);
    }

    /**
     * Returns the selection end offset.
     */
    public final int getSelectionEnd() {
        return selectionEnd;
    }

    /**
     * Returns the offset where the selection ends on the specified line.
     */
    public int getSelectionEnd(int line) {
        if (line == selectionEndLine) {
            return selectionEnd;
        } else if (rectSelect) {
            Element map = document.getDefaultRootElement();
            int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();

            Element lineElement = map.getElement(line);
            int lineStart = lineElement.getStartOffset();
            int lineEnd = lineElement.getEndOffset() - 1;
            return Math.min(lineEnd, lineStart + end);
        } else {
            return getLineEndOffset(line) - 1;
        }
    }

    /**
     * Returns the selection end line.
     */
    public final int getSelectionEndLine() {
        return selectionEndLine;
    }

    /**
     * Sets the selection end. The new selection will be the old selection start
     * and the bew selection end.
     *
     * @param selectionEnd The selection end
     * @see #select(int, int)
     */
    public final void setSelectionEnd(int selectionEnd) {
        select(selectionStart, selectionEnd);
    }

    /**
     * Returns the caret position. This will either be the selection start or the
     * selection end, depending on which direction the selection was made in.
     */
    public final int getCaretPosition() {
        return (biasLeft ? selectionStart : selectionEnd);
    }

    /**
     * Returns the caret line.
     */
    public final int getCaretLine() {
        return (biasLeft ? selectionStartLine : selectionEndLine);
    }

    /**
     * Returns the mark position. This will be the opposite selection bound to
     * the caret position.
     *
     * @see #getCaretPosition()
     */
    public final int getMarkPosition() {
        return (biasLeft ? selectionEnd : selectionStart);
    }

    /**
     * Returns the mark line.
     */
    public final int getMarkLine() {
        return (biasLeft ? selectionEndLine : selectionStartLine);
    }

    /**
     * Sets the caret position. The new selection will consist of the caret
     * position only (hence no text will be selected)
     *
     * @param caret The caret position
     * @see #select(int, int)
     */
    public final void setCaretPosition(int caret) {
        select(caret, caret);
    }

    /**
     * Selects all text in the document.
     */
    public final void selectAll() {
        select(0, getDocumentLength());
    }

    /**
     * Moves the mark to the caret position.
     */
    public final void selectNone() {
        select(getCaretPosition(), getCaretPosition());
    }

    /**
     * Selects from the start offset to the end offset. This is the general
     * selection method used by all other selecting methods. The caret position
     * will be start if start &lt; end, and end if end &gt; start.
     *
     * @param start The start offset
     * @param end   The end offset
     */
    public void select(int start, int end) {
        int newStart, newEnd;
        boolean newBias;
        if (start <= end) {
            newStart = start;
            newEnd = end;
            newBias = false;
        } else {
            newStart = end;
            newEnd = start;
            newBias = true;
        }

        if (newStart < 0 || newEnd > getDocumentLength()) {
            throw new IllegalArgumentException("Bounds out of" + " range: " + newStart + "," + newEnd);
        }

        // If the new position is the same as the old, we don't
        // do all this crap, however we still do the stuff at
        // the end (clearing magic position, scrolling)
        if (newStart != selectionStart || newEnd != selectionEnd || newBias != biasLeft) {
            int newStartLine = getLineOfOffset(newStart);
            int newEndLine = getLineOfOffset(newEnd);

            if (painter.isBracketHighlightEnabled()) {
                if (bracketLine != -1) {
                    painter.invalidateLine(bracketLine);
                }
                updateBracketHighlight(end);
                if (bracketLine != -1) {
                    painter.invalidateLine(bracketLine);
                }
            }

            painter.invalidateLineRange(selectionStartLine, selectionEndLine);
            painter.invalidateLineRange(newStartLine, newEndLine);

            document.addUndoableEdit(new CaretUndo(selectionStart, selectionEnd));

            selectionStart = newStart;
            selectionEnd = newEnd;
            selectionStartLine = newStartLine;
            selectionEndLine = newEndLine;
            biasLeft = newBias;

            fireCaretEvent();
        }

        // When the user is typing, etc, we don't want the caret
        // to blink
        blink = true;
        caretTimer.restart();

        // Disable rectangle select if selection start = selection end
        if (selectionStart == selectionEnd) {
            rectSelect = false;
        }

        // Clear the `magic' caret position used by up/down
        magicCaret = -1;

        scrollToCaret();
    }

    /**
     * Returns the selected text, or null if no selection is active.
     */
    public final String getSelectedText() {
        if (selectionStart == selectionEnd) {
            return null;
        }

        if (rectSelect) {
            // Return each row of the selection on a new line

            Element map = document.getDefaultRootElement();

            int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
            int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();

            // Certain rectangles satisfy this condition...
            if (end < start) {
                int tmp = end;
                end = start;
                start = tmp;
            }

            StringBuffer buf = new StringBuffer();
            Segment seg = new Segment();

            for (int i = selectionStartLine; i <= selectionEndLine; i++) {
                Element lineElement = map.getElement(i);
                int lineStart = lineElement.getStartOffset();
                int lineEnd = lineElement.getEndOffset() - 1;
                int lineLen = lineEnd - lineStart;

                lineStart = Math.min(lineStart + start, lineEnd);
                lineLen = Math.min(end - start, lineEnd - lineStart);

                getText(lineStart, lineLen, seg);
                buf.append(seg.array, seg.offset, seg.count);

                if (i != selectionEndLine) {
                    buf.append('\n');
                }
            }

            return buf.toString();
        } else {
            return getText(selectionStart, selectionEnd - selectionStart);
        }
    }

    /**
     * Replaces the selection with the specified text.
     *
     * @param selectedText The replacement text for the selection
     */
    public void setSelectedText(String selectedText) {
        if (!editable) {
            throw new InternalError("Text component" + " read only");
        }

        document.beginCompoundEdit();

        try {
            if (rectSelect) {
                Element map = document.getDefaultRootElement();

                int start = selectionStart - map.getElement(selectionStartLine).getStartOffset();
                int end = selectionEnd - map.getElement(selectionEndLine).getStartOffset();

                // Certain rectangles satisfy this condition...
                if (end < start) {
                    int tmp = end;
                    end = start;
                    start = tmp;
                }

                int lastNewline = 0;
                int currNewline = 0;

                for (int i = selectionStartLine; i <= selectionEndLine; i++) {
                    Element lineElement = map.getElement(i);
                    int lineStart = lineElement.getStartOffset();
                    int lineEnd = lineElement.getEndOffset() - 1;
                    int rectStart = Math.min(lineEnd, lineStart + start);

                    document.remove(rectStart, Math.min(lineEnd - rectStart, end - start));

                    if (selectedText == null) {
                        continue;
                    }

                    currNewline = selectedText.indexOf('\n', lastNewline);
                    if (currNewline == -1) {
                        currNewline = selectedText.length();
                    }

                    document.insertString(rectStart, selectedText.substring(lastNewline, currNewline), null);

                    lastNewline = Math.min(selectedText.length(), currNewline + 1);
                }

                if (selectedText != null && currNewline != selectedText.length()) {
                    int offset = map.getElement(selectionEndLine).getEndOffset() - 1;
                    document.insertString(offset, "\n", null);
                    document.insertString(offset + 1, selectedText.substring(currNewline + 1), null);
                }
            } else {
                document.remove(selectionStart, selectionEnd - selectionStart);
                if (selectedText != null) {
                    document.insertString(selectionStart, selectedText, null);
                }
            }
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
            throw new InternalError("Cannot replace" + " selection");
        }
        // No matter what happends... stops us from leaving document
        // in a bad state
        finally {
            document.endCompoundEdit();
        }

        setCaretPosition(selectionEnd);
    }

    /**
     * Returns true if this text area is editable, false otherwise.
     */
    public final boolean isEditable() {
        return editable;
    }

    /**
     * Sets if this component is editable.
     *
     * @param editable True if this text area should be editable, false otherwise
     */
    public void setEditable(boolean editable) {
        this.editable = editable;
    }

    /**
     * Returns the right click popup menu.
     */
    public final JPopupMenu getRightClickPopup() {
        return popup;
    }

    /**
     * Sets the right click popup menu.
     *
     * @param popup The popup
     */
    public final void setRightClickPopup(JPopupMenu popup) {
        this.popup = popup;
    }

    /**
     * Returns the `magic' caret position. This can be used to preserve the
     * column position when moving up and down lines.
     */
    public final int getMagicCaretPosition() {
        return magicCaret;
    }

    /**
     * Sets the `magic' caret position. This can be used to preserve the column
     * position when moving up and down lines.
     *
     * @param magicCaret The magic caret position
     */
    public final void setMagicCaretPosition(int magicCaret) {
        this.magicCaret = magicCaret;
    }

    /**
     * Similar to <code>setSelectedText()</code>, but overstrikes the appropriate
     * number of characters if overwrite mode is enabled.
     *
     * @param str The string
     * @see #setSelectedText(String)
     * @see #isOverwriteEnabled()
     */
    public void overwriteSetSelectedText(String str) {
        // Don't overstrike if there is a selection
        if (!overwrite || selectionStart != selectionEnd) {
            setSelectedText(str);
            return;
        }

        // Don't overstrike if we're on the end of
        // the line
        int caret = getCaretPosition();
        int caretLineEnd = getLineEndOffset(getCaretLine());
        if (caretLineEnd - caret <= str.length()) {
            setSelectedText(str);
            return;
        }

        document.beginCompoundEdit();

        try {
            document.remove(caret, str.length());
            document.insertString(caret, str, null);
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
        } finally {
            document.endCompoundEdit();
        }
    }

    /**
     * Returns true if overwrite mode is enabled, false otherwise.
     */
    public final boolean isOverwriteEnabled() {
        return overwrite;
    }

    /**
     * Sets if overwrite mode should be enabled.
     *
     * @param overwrite True if overwrite mode should be enabled, false otherwise.
     */
    public final void setOverwriteEnabled(boolean overwrite) {
        this.overwrite = overwrite;
        painter.invalidateSelectedLines();
    }

    /**
     * Returns true if the selection is rectangular, false otherwise.
     */
    public final boolean isSelectionRectangular() {
        return rectSelect;
    }

    /**
     * Sets if the selection should be rectangular.
     *
     * @param overwrite True if the selection should be rectangular, false otherwise.
     */
    public final void setSelectionRectangular(boolean rectSelect) {
        this.rectSelect = rectSelect;
        painter.invalidateSelectedLines();
    }

    /**
     * Returns the position of the highlighted bracket (the bracket matching the
     * one before the caret)
     */
    public final int getBracketPosition() {
        return bracketPosition;
    }

    /**
     * Returns the line of the highlighted bracket (the bracket matching the one
     * before the caret)
     */
    public final int getBracketLine() {
        return bracketLine;
    }

    /**
     * Adds a caret change listener to this text area.
     *
     * @param listener The listener
     */
    public final void addCaretListener(CaretListener listener) {
        listenerList.add(CaretListener.class, listener);
    }

    /**
     * Removes a caret change listener from this text area.
     *
     * @param listener The listener
     */
    public final void removeCaretListener(CaretListener listener) {
        listenerList.remove(CaretListener.class, listener);
    }

    /**
     * Deletes the selected text from the text area and places it into the
     * clipboard.
     */
    public void cut() {
        if (editable) {
            copy();
            setSelectedText("");
        }
    }

    /**
     * Places the selected text into the clipboard.
     */
    public void copy() {
        if (selectionStart != selectionEnd) {
            Clipboard clipboard = getToolkit().getSystemClipboard();

            String selection = getSelectedText();

            int repeatCount = inputHandler.getRepeatCount();
            StringBuffer buf = new StringBuffer();
            for (int i = 0; i < repeatCount; i++) {
                buf.append(selection);
            }

            clipboard.setContents(new StringSelection(buf.toString()), null);
        }
    }

    /**
     * Inserts the clipboard contents into the text.
     */
    public void paste() {
        if (editable) {
            Clipboard clipboard = getToolkit().getSystemClipboard();
            try {
                // The MacOS MRJ doesn't convert \r to \n,
                // so do it here
                String selection = ((String) clipboard.getContents(this).getTransferData(DataFlavor.stringFlavor))
                        .replace('\r', '\n');

                // after previous in come cases we get double \n so just remove one
                selection = selection.replace("\n\n", "\n");

                int repeatCount = inputHandler.getRepeatCount();
                StringBuffer buf = new StringBuffer();
                for (int i = 0; i < repeatCount; i++) {
                    buf.append(selection);
                }
                selection = buf.toString();
                setSelectedText(selection);
            } catch (Exception e) {
                getToolkit().beep();
                System.err.println("Clipboard does not" + " contain a string");
            }
        }
    }

    /**
     * Called by the AWT when this component is removed from it's parent. This
     * stops clears the currently focused component.
     */
    public void removeNotify() {
        super.removeNotify();
        if (focusedComponentRef != null && focusedComponentRef.get() == this) {
            focusedComponentRef = null;
        }

        if (this.document != null) {
            this.document.removeDocumentListener(documentHandler);
        }
    }

    @Override
    public void addNotify() {
        super.addNotify();

        if (this.document != null) {
            this.document.addDocumentListener(documentHandler);
        }
    }

    /**
     * Forwards key events directly to the input handler. This is slightly faster
     * than using a KeyListener because some Swing overhead is avoided.
     */
    public void processKeyEvent(KeyEvent evt) {
        if (inputHandler == null) {
            return;
        }
        switch (evt.getID()) {
            case KeyEvent.KEY_TYPED:
                inputHandler.keyTyped(evt);
                break;
            case KeyEvent.KEY_PRESSED:
                inputHandler.keyPressed(evt);
                break;
            case KeyEvent.KEY_RELEASED:
                inputHandler.keyReleased(evt);
                break;
        }

        if (!evt.isConsumed()) {
            KeyListener[] keyListeners = getKeyListeners();
            for (KeyListener listener : keyListeners) {
                switch (evt.getID()) {
                    case KeyEvent.KEY_TYPED:
                        listener.keyTyped(evt);
                        break;
                    case KeyEvent.KEY_PRESSED:
                        listener.keyPressed(evt);
                        break;
                    case KeyEvent.KEY_RELEASED:
                        listener.keyReleased(evt);
                        break;
                }

                if (evt.isConsumed()) {
                    break;
                }
            }

            if (!evt.isConsumed()) {
                getParent().dispatchEvent(evt);
            }
        }
    }

    // protected members
    protected static final String CENTER = "center";
    protected static final String RIGHT = "right";
    protected static final String BOTTOM = "bottom";

    protected static WeakReference<JEditTextArea> focusedComponentRef;
    protected static final Timer caretTimer;

    protected TextAreaPainter painter;

    protected JPopupMenu popup;

    protected EventListenerList listenerList;
    protected MutableCaretEvent caretEvent;

    protected boolean caretBlinks;
    protected boolean caretVisible;
    protected boolean blink;

    protected boolean editable;

    protected int firstLine;
    protected int visibleLines;
    // protected int electricScroll;

    // protected int horizontalOffset;

    // protected JScrollBar vertical;
    // protected JScrollBar horizontal;
    protected boolean scrollBarsInitialized;

    protected InputHandler inputHandler;
    protected SyntaxDocument document;
    protected DocumentHandler documentHandler;

    protected Segment lineSegment;

    protected int selectionStart;
    protected int selectionStartLine;
    protected int selectionEnd;
    protected int selectionEndLine;
    protected boolean biasLeft;

    protected int bracketPosition;
    protected int bracketLine;

    protected int magicCaret;
    protected boolean overwrite;
    protected boolean rectSelect;

    protected void fireCaretEvent() {
        Object[] listeners = listenerList.getListenerList();
        for (int i = listeners.length - 2; i >= 0; i--) {
            if (listeners[i] == CaretListener.class) {
                ((CaretListener) listeners[i + 1]).caretUpdate(caretEvent);
            }
        }
    }

    protected void updateBracketHighlight(int newCaretPosition) {
        if (newCaretPosition == 0) {
            bracketPosition = bracketLine = -1;
            return;
        }

        try {
            int offset = TextUtilities.findMatchingBracket(document, newCaretPosition - 1);
            if (offset != -1) {
                bracketLine = getLineOfOffset(offset);
                bracketPosition = offset - getLineStartOffset(bracketLine);
                return;
            }
        } catch (BadLocationException bl) {
            SoapUI.logError(bl);
        }

        bracketLine = bracketPosition = -1;
    }

    protected void documentChanged(DocumentEvent evt) {
        DocumentEvent.ElementChange ch = evt.getChange(document.getDefaultRootElement());

        int count;
        if (ch == null) {
            count = 0;
        } else {
            count = ch.getChildrenAdded().length - ch.getChildrenRemoved().length;
        }

        int line = getLineOfOffset(evt.getOffset());
        if (count == 0) {
            painter.invalidateLine(line);
        }
        // do magic stuff
        else if (line < firstLine) {
            setFirstLine(firstLine + count);
        }
        // end of magic stuff
        else {
            painter.invalidateLineRange(line, firstLine + visibleLines);
            updateScrollBars();
        }
    }

    class ScrollLayout implements LayoutManager {
        public void addLayoutComponent(String name, Component comp) {
            if (name.equals(CENTER)) {
                center = comp;
            }
			/*
			 * else if(name.equals(RIGHT)) right = comp; else
			 * if(name.equals(BOTTOM)) bottom = comp; else
			 * if(name.equals(LEFT_OF_SCROLLBAR)) leftOfScrollBar.addElement(comp);
			 */
        }

        public void removeLayoutComponent(Component comp) {
            if (center == comp) {
                center = null;
            }
			/*
			 * if(right == comp) right = null; if(bottom == comp) bottom = null;
			 * else leftOfScrollBar.removeElement(comp);
			 */
        }

        public Dimension preferredLayoutSize(Container parent) {
            Dimension dim = new Dimension();
            Insets insets = getInsets();
            dim.width = insets.left + insets.right;
            dim.height = insets.top + insets.bottom;

            Dimension centerPref = center.getPreferredSize();
            dim.width += centerPref.width;
            dim.height += centerPref.height;
			/*
			 * Dimension rightPref = right.getPreferredSize(); dim.width +=
			 * rightPref.width; Dimension bottomPref = bottom.getPreferredSize();
			 * dim.height += bottomPref.height;
			 */
            return dim;
        }

        public Dimension minimumLayoutSize(Container parent) {
            Dimension dim = new Dimension();
            Insets insets = getInsets();
            dim.width = insets.left + insets.right;
            dim.height = insets.top + insets.bottom;

            Dimension centerPref = center.getMinimumSize();
            dim.width += centerPref.width;
            dim.height += centerPref.height;
			/*
			 * Dimension rightPref = right.getMinimumSize(); dim.width +=
			 * rightPref.width; Dimension bottomPref = bottom.getMinimumSize();
			 * dim.height += bottomPref.height;
			 */
            return dim;
        }

        public void layoutContainer(Container parent) {
            Dimension size = parent.getSize();
            Insets insets = parent.getInsets();
            int itop = insets.top;
            int ileft = insets.left;
            int ibottom = insets.bottom;
            int iright = insets.right;

            // int rightWidth = right.getPreferredSize().width;
            // int bottomHeight = bottom.getPreferredSize().height;
            int centerWidth = size.width - ileft - iright;
            int centerHeight = size.height - itop - ibottom;

            center.setBounds(ileft, itop, centerWidth, centerHeight);

			/*
			 * right.setBounds( ileft + centerWidth, itop, rightWidth,
			 * centerHeight);
			 * 
			 * // Lay out all status components, in order Enumeration status =
			 * leftOfScrollBar.elements(); while(status.hasMoreElements()) {
			 * Component comp = (Component)status.nextElement(); Dimension dim =
			 * comp.getPreferredSize(); comp.setBounds(ileft, itop + centerHeight,
			 * dim.width, bottomHeight); ileft += dim.width; }
			 * 
			 * bottom.setBounds( ileft, itop + centerHeight, size.width -
			 * rightWidth - ileft - iright, bottomHeight);
			 */
        }

        // private members
        private Component center;
        // private Component right;
        // private Component bottom;
        // private Vector<Component> leftOfScrollBar = new Vector<Component>();
    }

    static class CaretBlinker implements ActionListener {
        public void actionPerformed(ActionEvent evt) {
            if (focusedComponentRef != null && focusedComponentRef.get() != null && focusedComponentRef.get().hasFocus()) {
                focusedComponentRef.get().blinkCaret();
            }
        }
    }

    class MutableCaretEvent extends CaretEvent {
        MutableCaretEvent() {
            super(JEditTextArea.this);
        }

        public int getDot() {
            return getCaretPosition();
        }

        public int getMark() {
            return getMarkPosition();
        }
    }

    class AdjustHandler implements AdjustmentListener {
        public void adjustmentValueChanged(final AdjustmentEvent evt) {
            if (!scrollBarsInitialized) {
                return;
            }

            // If this is not done, mousePressed events accumilate
            // and the result is that scrolling doesn't stop after
            // the mouse is released
            SwingUtilities.invokeLater(new Runnable() {
                public void run() {
					/*
					 * if(evt.getAdjustable() == vertical)
					 * setFirstLine(vertical.getValue()); else
					 * setHorizontalOffset(-horizontal.getValue());
					 */
                }
            });
        }
    }

    class ComponentHandler extends ComponentAdapter {
        public void componentResized(ComponentEvent evt) {
            recalculateVisibleLines();
            scrollBarsInitialized = true;
        }
    }

    class DocumentHandler implements DocumentListener {
        public void insertUpdate(DocumentEvent evt) {
            documentChanged(evt);

            int offset = evt.getOffset();
            int length = evt.getLength();

            int newStart;
            int newEnd;

            if (selectionStart > offset || (selectionStart == selectionEnd && selectionStart == offset)) {
                newStart = selectionStart + length;
            } else {
                newStart = selectionStart;
            }

            if (selectionEnd >= offset) {
                newEnd = selectionEnd + length;
            } else {
                newEnd = selectionEnd;
            }

            select(newStart, newEnd);
        }

        public void removeUpdate(DocumentEvent evt) {
            documentChanged(evt);

            int offset = evt.getOffset();
            int length = evt.getLength();

            int newStart;
            int newEnd;

            if (selectionStart > offset) {
                if (selectionStart > offset + length) {
                    newStart = selectionStart - length;
                } else {
                    newStart = offset;
                }
            } else {
                newStart = selectionStart;
            }

            if (selectionEnd > offset) {
                if (selectionEnd > offset + length) {
                    newEnd = selectionEnd - length;
                } else {
                    newEnd = offset;
                }
            } else {
                newEnd = selectionEnd;
            }

            select(newStart, newEnd);
        }

        public void changedUpdate(DocumentEvent evt) {
        }
    }

    class DragHandler implements MouseMotionListener {
        public void mouseDragged(MouseEvent evt) {
            if (popup != null && popup.isVisible()) {
                return;
            }

            setSelectionRectangular((evt.getModifiers() & InputEvent.CTRL_MASK) != 0);
            select(getMarkPosition(), xyToOffset(evt.getX(), evt.getY()));
        }

        public void mouseMoved(MouseEvent evt) {
        }
    }

    class FocusHandler implements FocusListener {
        public void focusGained(FocusEvent evt) {
            if (isEditable()) {
                setCaretVisible(true);
            }
            focusedComponentRef = new WeakReference<JEditTextArea>(JEditTextArea.this);
        }

        public void focusLost(FocusEvent evt) {
            setCaretVisible(false);
            focusedComponentRef = null;
        }
    }

    class MouseHandler extends MouseAdapter {
        @Override
        public void mouseClicked(MouseEvent e) {
            if (popup != null && e.isPopupTrigger()) {
                doPopup(e);
            }
        }

        private void doPopup(MouseEvent evt) {
            popup.show(painter, evt.getX(), evt.getY());
        }

        @Override
        public void mouseReleased(MouseEvent e) {
            if (popup != null && e.isPopupTrigger()) {
                doPopup(e);
            }
        }

        public void mousePressed(MouseEvent evt) {
            requestFocus();

            // Focus events not fired sometimes?
            if (isEditable()) {
                setCaretVisible(true);
            }

            focusedComponentRef = new WeakReference<JEditTextArea>(JEditTextArea.this);

            if (popup != null && evt.isPopupTrigger()) {
                doPopup(evt);
                return;
            }

            if (evt.getButton() != MouseEvent.BUTTON1) {
                return;
            }

            int line = yToLine(evt.getY());
            int offset = xToOffset(line, evt.getX());
            int dot = getLineStartOffset(line) + offset;

            switch (evt.getClickCount()) {
                case 1:
                    doSingleClick(evt, line, offset, dot);
                    break;
                case 2:
                    // It uses the bracket matching stuff, so
                    // it can throw a BLE
                    try {
                        doDoubleClick(evt, line, offset, dot);
                    } catch (BadLocationException bl) {
                        SoapUI.logError(bl);
                    }
                    break;
                case 3:
                    doTripleClick(evt, line, offset, dot);
                    break;
            }
        }

        private void doSingleClick(MouseEvent evt, int line, int offset, int dot) {
            if ((evt.getModifiers() & InputEvent.SHIFT_MASK) != 0) {
                rectSelect = (evt.getModifiers() & InputEvent.CTRL_MASK) != 0;
                select(getMarkPosition(), dot);
            } else {
                setCaretPosition(dot);
            }
        }

        private void doDoubleClick(MouseEvent evt, int line, int offset, int dot) throws BadLocationException {
            // Ignore empty lines
            if (getTabExpandedLineLength(line) == 0) {
                return;
            }

            try {
                int bracket = TextUtilities.findMatchingBracket(document, Math.max(0, dot - 1));
                if (bracket != -1) {
                    int mark = getMarkPosition();
                    // Hack
                    if (bracket > mark) {
                        bracket++;
                        mark--;
                    }
                    select(mark, bracket);
                    return;
                }
            } catch (BadLocationException bl) {
                SoapUI.logError(bl);
            }

            // Ok, it's not a bracket... select the word
            String lineText = getLineText(line);
            char ch = lineText.charAt(Math.max(0, offset - 1));

            String noWordSep = (String) document.getProperty("noWordSep");
            if (noWordSep == null) {
                noWordSep = "";
            }

            // If the user clicked on a non-letter char,
            // we select the surrounding non-letters
            boolean selectNoLetter = (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1);

            int wordStart = 0;

            for (int i = offset - 1; i >= 0; i--) {
                ch = lineText.charAt(i);
                if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
                    wordStart = i + 1;
                    break;
                }
            }

            int wordEnd = lineText.length();
            for (int i = offset; i < lineText.length(); i++) {
                ch = lineText.charAt(i);
                if (selectNoLetter ^ (!Character.isLetterOrDigit(ch) && noWordSep.indexOf(ch) == -1)) {
                    wordEnd = i;
                    break;
                }
            }

            int lineStart = getLineStartOffset(line);
            select(lineStart + wordStart, lineStart + wordEnd);

			/*
			 * String lineText = getLineText(line); String noWordSep =
			 * (String)document.getProperty("noWordSep"); int wordStart =
			 * TextUtilities.findWordStart(lineText,offset,noWordSep); int wordEnd
			 * = TextUtilities.findWordEnd(lineText,offset,noWordSep);
			 * 
			 * int lineStart = getLineStartOffset(line); select(lineStart +
			 * wordStart,lineStart + wordEnd);
			 */
        }

        private void doTripleClick(MouseEvent evt, int line, int offset, int dot) {
            select(getLineStartOffset(line), getLineEndOffset(line) - 1);
        }
    }

    class CaretUndo extends AbstractUndoableEdit {
        private int start;
        private int end;

        CaretUndo(int start, int end) {
            this.start = start;
            this.end = end;
        }

        public boolean isSignificant() {
            return false;
        }

        public String getPresentationName() {
            return "caret move";
        }

        public void undo() throws CannotUndoException {
            super.undo();

            select(start, end);
        }

        public void redo() throws CannotRedoException {
            super.redo();

            select(start, end);
        }

        public boolean addEdit(UndoableEdit edit) {
            if (edit instanceof CaretUndo) {
                CaretUndo cedit = (CaretUndo) edit;
                start = cedit.start;
                end = cedit.end;
                cedit.die();

                return true;
            } else {
                return false;
            }
        }
    }

    static {
        caretTimer = new Timer(500, new CaretBlinker());
        caretTimer.setInitialDelay(500);
        caretTimer.start();
    }

    public Dimension getPreferredSize() {
        Dimension preferredSize = painter.getPreferredSize();

        if (getParent() instanceof JViewport) {
            JViewport viewport = (JViewport) getParent();
            Dimension size = viewport.getSize();

            preferredSize = new Dimension((int) (preferredSize.getWidth() < size.getWidth() ? size.getWidth()
                    : preferredSize.getWidth()), (int) (preferredSize.getHeight() < size.getHeight() ? size.getHeight()
                    : preferredSize.getHeight()));
        }

        return preferredSize;
    }

    public Dimension getMaximumSize() {
        return painter.getMaximumSize();
    }

    public Dimension getMinimumSize() {
        return painter.getMinimumSize();
    }

    public int getMaxLineLength() {
        int max = 0;

        for (int c = 0; c < getLineCount(); c++) {
            int lineLength = getTabExpandedLineLength(c);
            if (lineLength > max) {
                max = lineLength;
            }
        }

        return max;
    }

    public Dimension getPreferredScrollableViewportSize() {
        return getPreferredSize();
    }

    public int getScrollableBlockIncrement(Rectangle arg0, int arg1, int arg2) {
        return getFontMetrics(getFont()).getHeight() * 5;
    }

    public boolean getScrollableTracksViewportHeight() {
        return false;
    }

    public boolean getScrollableTracksViewportWidth() {
        return false;
    }

    public int getScrollableUnitIncrement(Rectangle arg0, int arg1, int arg2) {
        return getFontMetrics(getFont()).getHeight();
    }
}
